package de.herzog.bbp.controller;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.herzog.bbp.model.Klasse;
import de.herzog.bbp.model.Mannschaft;
import de.herzog.bbp.model.Spiel;
import de.herzog.bbp.model.Verein;
import de.herzog.bbp.view.SpielBeanSorter;
import de.herzog.bbp.view.bean.SpielBean;

public class SpieleCheck {

	private static Logger LOG = LoggerFactory.getLogger(SpieleCheck.class);

	public static enum Mode {
		KLASSEN, HALLEN, ALL
	}

	private List<SpielBean> spiele;
	private MultiMap<Mannschaft, SpielBean> heimSpieleProMannschaft;
	private MultiMap<Mannschaft, SpielBean> gastSpieleProMannschaft;
	private MultiMap<Klasse, SpielBean> spieleProKlasse;
	private MultiMap<Klasse, Mannschaft> mannschaftenProKlasse;
	private Set<Mannschaft> mannschaften;
	private List<String> messages;

	/* package */SpieleCheck(List<SpielBean> spiele) {
		this.spiele = new ArrayList<SpielBean>(spiele);
		SpielBeanSorter.HEIMVEREIN_SORT.sort(this.spiele);
		this.heimSpieleProMannschaft = new MultiMap<>();
		this.gastSpieleProMannschaft = new MultiMap<>();
		this.spieleProKlasse = new MultiMap<>();
		this.mannschaftenProKlasse = new MultiMap<>();
		this.mannschaften = new TreeSet<>();
		this.messages = new ArrayList<>();
	}

	/* package */List<String> getMessages() {
		return messages;
	}

	private void clearErrors() {
		for (SpielBean spielBean : spiele) {
			spielBean.setError(null);
			spielBean.setCheckError(SpieleTableController.HEIM_COLUMN, false);
			spielBean.setCheckError(SpieleTableController.GAST_COLUMN, false);
		}
	}

	private void initializeMaps() {
		for (SpielBean spielBean : spiele) {
			Spiel spiel = spielBean.getSpiel();
			heimSpieleProMannschaft.add(spiel.getHeimMannschaft(), spielBean);
			gastSpieleProMannschaft.add(spiel.getGastMannschaft(), spielBean);
			spieleProKlasse.add(spiel.getKlasse(), spielBean);
			mannschaftenProKlasse.add(spiel.getKlasse(), spiel.getHeimMannschaft());
			mannschaftenProKlasse.add(spiel.getKlasse(), spiel.getGastMannschaft());
			mannschaften.add(spiel.getHeimMannschaft());
			mannschaften.add(spiel.getGastMannschaft());
		}
	}

	/* package */void checkKlassen() {
		clearErrors();
		initializeMaps();
		for (Mannschaft mannschaft : mannschaften) {
			checkMannschaft(mannschaft);
		}
	}

	private void checkMannschaft(Mannschaft mannschaft) {
		long heimspielCount = heimSpieleProMannschaft.get(mannschaft).size();
		long gastspielCount = gastSpieleProMannschaft.get(mannschaft).size();
		long spielCount = heimspielCount + gastspielCount;
		long klassenspielCount = spieleProKlasse.get(mannschaft.getKlasse()).size();
		long mannschaftenCount = getMannschaftenCount(mannschaft, klassenspielCount);
		long rundenCount = Math.round(2 * klassenspielCount / (mannschaftenCount * (mannschaftenCount - 1)));
		long spielCountExpected = (mannschaftenCount - 1) * rundenCount;
		long heimGastDifference = Math.abs(heimspielCount - gastspielCount);

		if (spielCount != spielCountExpected || heimGastDifference > 1) {
			final String message = getErrorMessage(mannschaft, heimspielCount, gastspielCount, spielCount,
					spielCountExpected, heimGastDifference);
			LOG.warn(message);
			messages.add(message);
			for (SpielBean spiel : heimSpieleProMannschaft.get(mannschaft)) {
				spiel.setCheckError(SpieleTableController.HEIM_COLUMN, true);
				spiel.addError(message);
			}
			for (SpielBean spiel : gastSpieleProMannschaft.get(mannschaft)) {
				spiel.setCheckError(SpieleTableController.GAST_COLUMN, true);
				spiel.addError(message);
			}
		}
	}

	private long getMannschaftenCount(Mannschaft mannschaft, long klassenspielCount) {
		Set<Mannschaft> mannschaftenInDerKlasse = mannschaftenProKlasse.get(mannschaft.getKlasse());
		MultiMap<Verein, Mannschaft> mannschaftenProVerein = new MultiMap<>();
		mannschaftenInDerKlasse.forEach(m -> mannschaftenProVerein.add(m.getVerein(), m));
		if (mannschaftenInDerKlasse.size() == mannschaftenProVerein.size()) {
			return mannschaftenInDerKlasse.size();
		} else {
			long m = mannschaftenProVerein.size();
			long einfachRundeCount = m * (m - 1) / 2;
			if (klassenspielCount % einfachRundeCount == 0) {
				return mannschaftenProVerein.size();
			} else {
				return mannschaftenInDerKlasse.size();
			}
		}
	}

	private String getErrorMessage(Mannschaft mannschaft, long heimspielCount, long gastspielCount, long spielCount,
			long spielCountExpected, long heimGastDifference) {
		StringBuffer buffer = new StringBuffer(mannschaft.toString());
		if (spielCount != spielCountExpected) {
			buffer.append(": ");
			buffer.append(heimspielCount);
			buffer.append("H+");
			buffer.append(gastspielCount);
			buffer.append("G=");
			buffer.append(spielCount);
			buffer.append(" <> ");
			buffer.append(spielCountExpected);
		}
		if (heimGastDifference > 1) {
			buffer.append(": |");
			buffer.append(heimspielCount);
			buffer.append("H-");
			buffer.append(gastspielCount);
			buffer.append("G| > 1");
		}
		final String message = buffer.toString();
		return message;
	}

	/* package */void checkHallen() {
		Spiel letztesSpiel = null;
		SpielBean letztesSpielBean = null;
		for (SpielBean spielBean : spiele) {
			Spiel spiel = spielBean.getSpiel();
			if (letztesSpiel != null && isHallenDoppel(letztesSpiel, spiel)) {
				String message = spiel.getHalle() + " " + spielBean.getDatumString() + " " + spielBean.getZeit();
				LOG.warn(message);
				messages.add(message);
				letztesSpielBean.setCheckError(SpieleTableController.ZEIT_COLUMN, true);
				letztesSpielBean.addError(message);
				spielBean.setCheckError(SpieleTableController.ZEIT_COLUMN, true);
				spielBean.addError(message);
			}
			letztesSpiel = spiel;
			letztesSpielBean = spielBean;
		}
	}

	private boolean isHallenDoppel(Spiel spiel1, Spiel spiel2) {
		if (spiel1 == null || spiel2 == null) {
			return false;
		} else {
			boolean sameHalle = spiel1.getHalle().equals(spiel2.getHalle());
			boolean sameDatum = spiel1.getDatum().equals(spiel2.getDatum());
			int minutesDiff = Math.abs(spiel1.getZeitMinuten() - spiel2.getZeitMinuten());
			return sameHalle && sameDatum && minutesDiff < 60 + 45;
		}
	}

	public static List<String> check(List<SpielBean> spiele, Mode mode) {
		SpieleCheck spieleCheck = new SpieleCheck(spiele);
		if (mode == Mode.KLASSEN || mode == Mode.ALL) {
			spieleCheck.checkKlassen();
		}
		if (mode == Mode.HALLEN || mode == Mode.ALL) {
			spieleCheck.checkHallen();
		}
		LOG.info("Check for {} ended with {} errors", mode, spieleCheck.messages.size());
		return spieleCheck.messages;
	}

	/**
	 * An implementation of Map that stores multiple values per key.
	 *
	 * @param <K>
	 * @param <V>
	 */
	private class MultiMap<K, V> extends HashMap<K, Set<V>> {

		private static final long serialVersionUID = 1L;

		@Override
		public Set<V> get(Object key) {
			Set<V> set = super.get(key);
			return set == null ? new TreeSet<>() : set;
		}

		/**
		 * The implementation of "put" that stores multiple values per key. The
		 * method is not called "put" because it cannot override this existing
		 * method.
		 * 
		 * @param key
		 * @param value
		 * @return
		 */
		public Set<V> add(K key, V value) {
			Set<V> set = get(key);
			set.add(value);
			return put(key, set);
		}

	}
}
